﻿//
// Copyright (c) 2011 Western Data Solutions (www.westerndatasolutions.com) (Armen Shimoon)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
// documentation files (the "Software"), to deal in the Software without restriction, including without 
// limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 
// the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace WDS.Bencode.Parsing
{
    /// <summary>
    /// Parses a stream of bencoded data
    /// </summary>
    public class StreamParser
    {
        /// <summary>
        /// Incoming bencoded data reader
        /// </summary>
        private StreamReader _sr;
        /// <summary>
        /// Incoming bencoded data source
        /// </summary>
        private readonly Stream _st;

        /// <summary>
        /// Collector for currently processed tokens from incoming bencoded data stream
        /// </summary>
        private IList<TokenBase> _tokens;

        /// <summary>
        /// Default constructor
        /// </summary>
        /// <param name="st">Stream of bencoded data</param>
        public StreamParser(Stream st)
        {
            _st = st;
        }

        /// <summary>
        /// Parses any valid bencoded data into a generic <c>BencodeElement</c>, regardless of actual element type
        /// </summary>
        /// <returns></returns>
        public BencodeElement Parse()
        {
            try
            {

                CreateNewStreamReader();

                // Create empty collector for tokens
                _tokens = new List<TokenBase>();
                // read in the bencoded data, tokenizing as it goes
                AppendTokensFromStream();

                // close and displose of reader
                TryCloseAndDisposeReader();

                // valid bencoded data must have one single root element.
                // parsing should leave us with a single token, otherwise the data is incorrect.
                return GetSingleTokenValue(_tokens);
            }
            catch (BencodeParseException)
            {
                TryCloseAndDisposeReader();
                throw;
            }
            catch (Exception ex)
            {
                TryCloseAndDisposeReader();
                throw new BencodeParseException("Encountered unexpected exception while parsing: " + ex.Message, ex);
            }

        }

        /// <summary>
        /// Parses the stream of bencoded data as a strongly typed bencode element. 
        /// </summary>
        /// <typeparam name="T">The type (ie <c>BencodeDictionary</c>, <c>BencodeList</c>, and so on)</typeparam>
        /// <returns>A strongly typed representation of the bencoded data, or throws an exception if the expected type does not match the input type</returns>
        public T ParseAs<T>() where T : BencodeElement
        {
            BencodeElement element = null;
            try
            {
                element = Parse();
                return (T)element;
            }
            catch (InvalidCastException ex)
            {
                var elementType = "[unresolvable]";
                if (element != null) elementType = element.GetType().Name;

                throw new BencodeParseException(
                   "Requested data to be parsed as " + typeof(T).Name + " but was " + elementType,
                   ex);
            }
        }

        /// <summary>
        /// Cleanup after reading
        /// </summary>
        protected void TryCloseAndDisposeReader()
        {
            if (_sr == null) return;

            _sr.Close();
            _sr.Dispose();
            _sr = null;
        }

        /// <summary>
        /// A few mechanisms to gracefully create a new reader for the incoming bencoded data stream
        /// </summary>
        protected void CreateNewStreamReader()
        {

            if (_st == null)
            {
                throw new BencodeParseException(
                    "Input stream was null.");
            }

            if (_st.CanSeek) _st.Seek(0, SeekOrigin.Begin);
            _sr = new StreamReader(_st, Encoding.ASCII);

            if (!_st.CanRead)
            {
                throw new BencodeParseException("Cannot read from input stream.");
            }


            if (_sr == null)
            {
                throw new BencodeParseException(
                    "Stream reader is null after creation.");
            }
        }

        /// <summary>
        /// Returns the only element in the passed in list, or throws an exception if less or more are found.
        /// </summary>
        /// <remarks>This is because valid bencode data must reside within a single root node</remarks>
        /// <param name="tokens">The result of parsing</param>
        /// <returns>The one and only element in the passed in list</returns>
        protected static BencodeElement GetSingleTokenValue(IList<TokenBase> tokens)
        {
            if (tokens == null)
            {
                throw new BencodeParseException("List of tokens passed in was null.");
            }

            if (tokens.Count != 1)
            {
                throw new BencodeParseException(
                    string.Format(
                    "A bencoded file should only contain exactly one root element. {0} tokens were found.",
                    tokens.Count),
                    null,
                    tokens);
            }

            return tokens[0].Value;
        }

        /// <summary>
        /// Processes the entire stream of data, tokenizing the data as it goes
        /// </summary>
        private void AppendTokensFromStream()
        {

            while (StreamHasMoreTokens)
            {
                var fChar = _sr.Peek();

                if (fChar.Equals('i'))
                {
                    AppendIntegerToken();
                }
                else if (fChar.Equals('d'))
                {
                    AppendDictionaryStartToken();
                }
                else if (fChar.Equals('l'))
                {
                    AppendListStartToken();
                }
                else if (fChar.Equals('e'))
                {
                    AppendCollectionEndToken();
                }
                else
                {
                    AppendStringToken();
                }
            }
           
            
        }

        /// <summary>
        /// Wrapper for gracefully reading a single character from the stream
        /// </summary>
        /// <returns>A single character from the current position in the incoming data stream</returns>
        protected char ReadChar()
        {
            try
            {
                var intVal = _sr.Read();
                return (char)intVal;
            }
            catch (System.IO.IOException ioex)
            {
                throw new BencodeParseException("IO Exception while reading from source stream: " + ioex.Message, ioex);
            }

        }

        /// <summary>
        /// Parses a single integer value from the stream
        /// </summary>
        private void AppendIntegerToken()
        {
            var bytes = new List<char>();

            ReadChar();

            var currentChar = ReadChar();
            while (!currentChar.Equals('e'))
            {
                bytes.Add(currentChar);
                currentChar = ReadChar();
            }

            var intData = new string(bytes.ToArray());

            int intValue;
            if (int.TryParse(intData, out intValue))
            {
                _tokens.Add(new IntegerToken { Value = new BencodeInteger(intValue) });
            }
            else
            {
                throw new BencodeParseException(
                    "Failed attempting to parse " + intData + " as an integer.",
                    null,
                    _tokens);
            }
        }

        /// <summary>
        /// Wrapper for determining whether to continue or stop
        /// </summary>
        protected bool StreamHasMoreTokens
        {
            get
            {
                return !_sr.EndOfStream;
            }
        }

        /// <summary>
        /// Parses a dictionary start value from the stream into a dictionary start token
        /// </summary>
        private void AppendDictionaryStartToken()
        {
            ReadChar();

            _tokens.Add(new DictionaryStartToken());

            // You will never see a dictionary start token at the end of a bencoded data stream
            if (!StreamHasMoreTokens)
            {
                throw new BencodeParseException(
               "Was expecting dictionary items but string unexpectedly ended.",null,_tokens);
            }
           
        }

        /// <summary>
        /// Parses a list start value from the stream into a list start token
        /// </summary>
        private void AppendListStartToken()
        {
            ReadChar();

            _tokens.Add(new ListStartToken());

            // You will never see a list start token at the end of a bencoded data stream
            if (!StreamHasMoreTokens)
            {
                throw new BencodeParseException("Was expecting list items but string unexpectedly ended.",null,_tokens);
            }
           
        }

        /// <summary>
        /// When the end of a collection is reached, the collection elements are then "closed" into their containing collection
        /// </summary>
        private void AppendCollectionEndToken()
        {
            // read the end of the collection marker
            ReadChar();

            // mark the end of the collection with a collection end token
            _tokens.Add(new CollectionEndToken());
            // collect all tokens between the collection start and end tokens
            CloseCollection();
     
        }

        /// <summary>
        /// Parses a variable-length string into a string token
        /// </summary>
        private void AppendStringToken()
        {

            var bytes = new List<char>();

            var currentChar = ReadChar();

            while (!currentChar.Equals(':'))
            {
                bytes.Add(currentChar);
                currentChar = ReadChar();
            }

            var dataLengthString = new string(bytes.ToArray());

            int dataLength;
            if (int.TryParse(dataLengthString, out dataLength))
            {
                var dataBuffer = new char[dataLength];

                _sr.Read(dataBuffer, 0, dataLength);

                _tokens.Add(new StringToken { Value = new BencodeString(new string(dataBuffer)) });
              
            }
            else
            {
                 throw new BencodeParseException("Error while parsing string length",
                null,
                _tokens);
            }
           
        }

        /// <summary>
        /// Collects all element between the last collection end token and collection start token into a single collection element (dictionary or list)
        /// </summary>
        private void CloseCollection()
        {
            var collectionStartToken = GetLastOccurenceOf<ICollectionStartToken>();
            var collectionEndToken = GetLastOccurenceOf<CollectionEndToken>();


            if (collectionStartToken == null)
            {
                throw new BencodeParseException(
                    "Encountered a collection end token, but a matching collection start token was not found.",
                    null,
                    _tokens);
            }

            if (collectionEndToken == null)
            {
                throw new BencodeParseException(
                    "Attempted to populate a dictionary or list, but the end collection token wasn't found.",
                    null,
                    _tokens);
            }


            var idxStartCollection = _tokens.IndexOf(collectionStartToken);
            var idxEndCollection = _tokens.IndexOf(collectionEndToken);

            IList<TokenBase> newTokens = new List<TokenBase>();

            // create a new list for our set of currently processed tokens
            // gather up all the tokens up till the beginning of the collection being closed
            for (int i = 0; i < idxStartCollection; i++)
            {
                newTokens.Add(_tokens[i]);
            }

            // If we are processing a dictionary, the key-value pairs come one after another
            if (typeof(DictionaryStartToken).IsAssignableFrom(collectionStartToken.GetType()))
            {
                var dict = new DictionaryToken();
                var benDict = new BencodeDictionary();

                // step through each key value pair
                for (int j = idxStartCollection + 1; j < idxEndCollection; j += 2)
                {
                    try
                    {
                        benDict.Add(((BencodeString)_tokens[j].Value), _tokens[j + 1].Value);
                    }
                    catch (InvalidCastException ex)
                    {
                        throw new BencodeParseException(
                            "Expected a key of BencodeString type, but got type " + _tokens[j].Value.GetType().Name + " instead.",
                            ex,
                            _tokens);
                    }

                }
                dict.Value = benDict;
                // insert closed dictionary collection into new list of processed tokens
                newTokens.Add(dict);
            }
            // go though list elements sequentially
            else if (typeof(ListStartToken).IsAssignableFrom(collectionStartToken.GetType()))
            {
                var list = new ListToken();
                var benList = new BencodeList();

                for (int j = idxStartCollection + 1; j < idxEndCollection; j++)
                {
                    benList.Add(_tokens[j].Value);
                }
                list.Value = benList;
                // insert closed list collection into new list of processed tokens
                newTokens.Add(list);
            }
            else
            {
                throw new BencodeParseException(
                    "Found collection start token, but it was not castble to a Dictionary start token or List start token. It was type " + collectionStartToken.GetType().Name,
                    null,
                    _tokens);
            }
            // add all existing items to new list of processed tokens
            for (int k = idxEndCollection + 1; k <_tokens.Count; k++)
            {
                newTokens.Add(_tokens[k]);
            }

            _tokens= newTokens;

        }

        /// <summary>
        /// Returns the final occurence of a given token in the currently processed tokens collection, and throws an exception if an occurence is not found
        /// </summary>
        /// <typeparam name="T">The type of the bencode element token desired</typeparam>
        /// <returns>Weakly typed token representing the token requested</returns>
        private TokenBase GetLastOccurenceOf<T>()
        {
            for (int i = _tokens.Count - 1; i >= 0; i--)
            {
                if (typeof(T).IsAssignableFrom(_tokens[i].GetType()))
                {
                    return _tokens[i];
                }
            }
            throw new BencodeParseException("Couldn't find last occurence of " + typeof(T).Name + ".");
        }

    }
}
